module RichUnits

  # Durations
  #
  class Duration
    include Comparable

    SECOND =   1
    MINUTE =  60 * SECOND
    HOUR   =  60 * MINUTE
    DAY    =  24 * HOUR
    WEEK   =   7 * DAY
    YEAR   = 365 * DAY

    SEGMENTS = %w{years weeks days hours minutes seconds}.collect{ |s| s.to_sym }

    # Same as #new
    #
    def self.[](seconds, segmentA=nil, segmentB=nil)
      new(seconds, segmentA, segmentB)
    end

    # New duration.
    #
    #   call-seq:
    #     new(seconds)
    #     new(seconds, max-period)
    #     new(seconds, max-period, min-period)
    #     new(seconds, [period1, period2, ...])
    #
    def initialize(seconds=0, segmentA=nil, segmentB=nil)
      @seconds = seconds.to_i
      reset_segments(segmentA, segmentB)
    end

    # List of period segments.
    def segments; @segments; end

    # Reset segments.
    #
    #   call-seq:
    #     reset_segments(max-period)
    #     reset_segments(max-period, min-period)
    #     reset_segments([period1, period2, ...])
    #
    def reset_segments(segmentA=nil, segmentB=nil)
      if !segmentA
        @segments = [:days, :hours, :minutes, :seconds]
      elsif !segmentB
        case segmentA
        when Array
          @segments = segmentA.map{ |p| (p.to_s.downcase.chomp('s') + 's').to_sym }
          raise ArgumentError unless @segments.all?{ |s| SEGMENTS.include?(s) }
        else
          f = SEGMENTS.index(segmentA)
          @segments = SEGMENTS[f..0]
        end
      else # segmentA && segmentB
        f = SEGMENTS.index(segmentA)
        t = SEGMENTS.index(segmentB)
        @segments = SEGMENTS[f..t]
      end
    end

    def inspect
      h = to_h
      segments.reverse.collect do |l|
        "#{h[l.to_sym]} #{l}"
      end.join(' ')
    end

    def to_i ; @seconds.to_i ; end
    def to_f ; @seconds.to_f ; end

    alias_method :to_int, :to_i

    public

    def to_a
      a, s = [], @seconds
      a[5], s = *s.divmod(YEAR)   if @segments.include?(:years)
      a[4], s = *s.divmod(WEEK)   if @segments.include?(:weeks)
      a[3], s = *s.divmod(DAY)    if @segments.include?(:days)
      a[2], s = *s.divmod(HOUR)   if @segments.include?(:hours)
      a[1], s = *s.divmod(MINUTE) if @segments.include?(:minutes)
      a[0], s = *s.divmod(SECOND) if @segments.include?(:seconds)
      a.compact.reverse
    end

    #
    def to_h
      h, s = {}, @seconds
      h[:years],   s = *s.divmod(YEAR)   if @segments.include?(:years)
      h[:weeks],   s = *s.divmod(WEEK)   if @segments.include?(:weeks)
      h[:days],    s = *s.divmod(DAY)    if @segments.include?(:days)
      h[:hours],   s = *s.divmod(HOUR)   if @segments.include?(:hours)
      h[:minutes], s = *s.divmod(MINUTE) if @segments.include?(:minutes)
      h[:seconds], s = *s.divmod(SECOND) if @segments.include?(:seconds)
      h
    end

    def to_s
      h = to_h
      segments.reverse.collect do |l|
        "#{h[l.to_sym]} #{l}"
      end.join(' ')
    end

    # Returns true if <tt>other</tt> is also a Duration instance with the
    # same <tt>value</tt>, or if <tt>other == value</tt>.
    def ==(other)
      if Duration === other
        other.seconds == seconds
      else
        other == seconds
      end
    end

    def <=>(other)
      @seconds <=> other.to_i
    end

    #def is_a?(klass) #:nodoc:
    #  klass == self.class
    #end
  
    #def self.===(other) #:nodoc:
    #  other.is_a?(Duration) rescue super
    #end

    def years   ; to_h[:years]   ; end
    def weeks   ; to_h[:weeks]   ; end
    def days    ; to_h[:days]    ; end
    def hours   ; to_h[:hours]   ; end
    def minutes ; to_h[:minutes] ; end
    def seconds ; to_h[:seconds] ; end

    def total ; seconds ; end

    def +(other)
      self.class.new(@seconds + other.to_i, segments)
    end

    def -(other)
      self.class.new(@seconds - other.to_i, segments)
    end

    def *(other)
      self.class.new(@seconds * other.to_i, segments)
    end

    def /(other)
      self.class.new(@seconds / other.to_i, segments)
    end

    #
    def segmented(*segments)
      self.class.new(@seconds, segments)
      #segments = segments.collect{ |p| p.to_s.downcase.chomp('s') }
      #y,w,d,h,m,s = nil,nil,nil,nil,nil,nil
      #x = @seconds
      #y, x = *x.divmod(YEAR)   if segments.include?('year')
      #w, x = *x.divmod(WEEK)   if segments.include?('week')
      #d, x = *x.divmod(DAY)    if segments.include?('day')
      #h, x = *x.divmod(HOUR)   if segments.include?('hour')
      #m, x = *x.divmod(MINUTE) if segments.include?('minute')
      #s = x if segments.include?('second')
      #[y, w, d, h, m, s].compact
    end

    # Format duration.
    #
    # *Identifiers*
    #
    #     %w -- Number of weeks
    #     %d -- Number of days
    #     %h -- Number of hours
    #     %m -- Number of minutes
    #     %s -- Number of seconds
    #     %t -- Total number of seconds
    #     %x -- Duration#to_s
    #     %% -- Literal `%' character
    #
    # *Example*
    #
    #     d = Duration.new(:weeks => 10, :days => 7)
    #     => #<Duration: 11 weeks>
    #     d.strftime("It's been %w weeks!")
    #     => "It's been 11 weeks!"
    #
    def strftime(fmt)
      h = to_h
      hx = {
       'y' => h[:years]  ,
       'w' => h[:weeks]  ,
       'd' => h[:days]   ,
       'h' => h[:hours]  ,
       'm' => h[:minutes],
       's' => h[:seconds],
       't' => total,
       'x' => to_s 
      }
      fmt.gsub(/%?%(w|d|h|m|s|t|x)/) do |match|
        hx[match[1..1]]
      end.gsub('%%', '%')
    end

    #
    def -@ #:nodoc:
      self.class.new(-@seconds)
    end

    #
    def +@ #:nodoc:
      self.class.new(+@seconds)
    end

    #
    # Need to wrap back to numeric methods, maybe use method_missing?
    #

    #
    def before(time)
      @seconds.before(time)
    end

    #
    def after(time)
      @seconds.after(time)
    end


    # = Numeric Extensions for Durations
    #
    module Numeric

      # Enables the use of time calculations and declarations,
      # like 45.minutes + 2.hours + 4.years. The base unit for
      # all of these Numeric time methods is seconds.
      def seconds ; Duration[self] ; end
      alias_method :second, :seconds

      # Converts minutes into seconds.
      def minutes ; Duration[self * 60] ; end
      alias_method :minute, :minutes

      # Converts hours into seconds.
      def hours ; Duration[self * 3600] ; end
      alias_method :hour, :hours
      #def as_hours ; self / 60.minutes ; end

      # Converts days into seconds.
      def days ; Duration[self * 86400] ; end
      alias_method :day, :days

      # Converts weeks into seconds.
      def weeks ; Duration[self * 604800] ; end
      alias_method :week, :weeks

      # Converts fortnights into seconds.
      # (A fortnight is 2 weeks)
      def fortnights ; Duration[self * 1209600] ; end
      alias_method :fortnight, :fortnights

      # Converts months into seconds.
      # WARNING: This is not exact as it assumes 30 days to a month.
      def months ; Duration[self * 30 * 86400] ; end
      alias_method :month, :months

      # Converts years into seconds.
      # WARNING: This is not exact as it assumes 365 days to a year.
      #          ie. It doesn not account for leap years.
      def years ; Duration[self * 365 * 86400, :years] ; end
      alias_method :year, :years

    end

    # Time#duration has been added to convert the UNIX timestamp into a Duration.
    # See Time#duration for an example.
    #
    module Time
      # Create a Duration object from the UNIX timestamp.
      #
      # *Example*
      #
      #   Time.now.duration
      #   => #<Duration: 1898 weeks, 6 days, 1 hour, 12 minutes and 1 second>
      #
      def duration
        Duration[to_i]
      end
    end

  end

end

class Numeric #:nodoc:
  include RichUnits::Duration::Numeric
end

class Time #:nodoc:
  include RichUnits::Duration::Time
end

